using System;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using System.Diagnostics.Contracts;

namespace Granite.Data
{

    /// <summary>
    /// A SqlServerConnection with transactional support.
    /// </summary>
    /// <remarks>This class is not threadsafe.</remarks>
    public class TransactionalSqlServerConnection : SqlServerConnection, IDisposable, ITransactionalConnection
    {
        readonly SqlConnection m_Connection;

        bool m_Disposed;

        readonly SqlTransaction m_Transaction;

        internal TransactionalSqlServerConnection(SqlServerConnection parentConnection, bool forwardEvents)
            : base(parentConnection.ConnectionString, parentConnection.DefaultTimeout)
        {
			Contract.Requires(parentConnection != null);

            m_Connection = CreateSqlConnection();
            m_Transaction = m_Connection.BeginTransaction();

            if (forwardEvents)
            {
                ProcedureCallStarted += (sender, e) => parentConnection.OnProcedureCallStarted(e);
                ProcedureCallFinished += (sender, e) => parentConnection.OnProcedureCallFinished(e);
                ProcedureCallError += (sender, e) => parentConnection.OnProcedureCallError(e);
            }

        }

        /// <summary>
        /// Commits the current transaction and closes the associated connection.
        /// </summary>
        public void Commit()
        {
            if (m_Disposed)
                throw new InvalidOperationException("Transaction is disposed");

            m_Transaction.Commit();
            Dispose(true);

        }

        /// <summary>
        /// Reverts the pending changes
        /// </summary>
        public void Rollback()
        {
            if (m_Disposed)
                throw new InvalidOperationException("Transaction is disposed");

            m_Transaction.Rollback();
            Dispose(true);
        }

        /// <summary>
        /// Closes the current transaction and connection. If not committed, the transaction is rolled back.
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                m_Transaction.Dispose();
                m_Connection.Dispose();
                m_Disposed = true;
            }
        }

        /// <summary>
        /// Closes the current transaction and connection. If not committed, the transaction is rolled back.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Attaches the transaction to the newly created SqlCommand
        /// </summary>
        /// <param name="procedure"></param>
        /// <returns></returns>
		SqlCommand CreateStoredProcedure(ProcedureCall procedure)
		{
			if (procedure == null)
				throw new ArgumentNullException("procedure", "procedure is null.");
			Contract.EndContractBlock();

			var temp = base.CreateStoredProcedure(procedure, m_Connection);
			temp.Transaction = m_Transaction;
			return temp;
		}

        /// <summary>
        /// Creates a new transaction
        /// </summary>
        /// <param name="forwardEvents">If true, logging events are forwarded to the parent connection. </param>
        /// <returns></returns>
        public override ITransactionalConnection BeginTransaction(bool forwardEvents)
        {
			//TODO: Fix this so that it support nested transactions.
            throw new NotSupportedException("Nested transactions are not supported");
        }

        /// <summary>
        /// Indicates if transaction support is available
        /// </summary>
        public override bool CanBeginTransaction
        {
            get { return false; }
        }

        /// <summary>
        /// Extension point for overriding how procedures are executed.
        /// </summary>
        /// <param name="procedure"></param>
        /// <param name="action">This receives a SqlCommand that has been executed.</param>
        protected override void ExecuteCore(ProcedureCall procedure, Action<SqlCommand> action)
        {
            if (procedure == null)
                throw new ArgumentNullException("procedure", "procedure is null.");
            if (action == null)
                throw new ArgumentNullException("action", "action is null.");
			Contract.EndContractBlock();

            var startTime = DateTimeOffset.Now;
            OnProcedureCallStarted(procedure, startTime);
            try
            {
                using (var cmd = CreateStoredProcedure(procedure))
                {
                    action(cmd);
                }

                OnProcedureCallFinished(procedure, startTime, DateTimeOffset.Now);
            }
            catch (SqlException ex)
            {
                ex.Data["ConnectionString"] = ConnectionString;
                ex.Data["ProcedureCall"] = procedure.ToString();
                OnProcedureCallError(procedure, startTime, DateTimeOffset.Now, ex);
                throw;
            }
            catch (SqlTypeException ex)
            {
                ex.Data["ConnectionString"] = ConnectionString;
                ex.Data["ProcedureCall"] = procedure.ToString();
                OnProcedureCallError(procedure, startTime, DateTimeOffset.Now, ex);
                throw;
            }
        }
    }
}
